/*
	Copyright (c) 2009-2011, Stefan Webb.

	This file is part of SimpleML. Please read the file "license.txt", 
	which contains the conditions under which this software is distributed.
*/

/*!
	\file text.cpp
	\author Stefan Webb
	\brief Definition of Text class methods.
*/

#include "stdafx.h"

#include <stdio.h>
#include <stdlib.h>

/*! Constructor with filename to open. */
Text::Text(wchar_t* file)
{
	// Initialize variables
	initialize();
	
	// Open file
	open(file);
}

/*! Default constructor. */
Text::Text()
{
	// Initialize variables
	initialize();
}

/*! Default destructor */
Text::~Text()
{
	close();
}

/* Initialize variables */
void Text::initialize()
{
	text_ = 0;
	length_ = 0;
	size_ = 0;
	buffer_ = 0;
	numberlines = 0;
	lines = 0;
}

/* Detects the encoding of a filebuffer if it is a text file. */
Encoding Text::detectEncoding(unsigned char* src)
{
	// We must have at least two characters for UTF-16
	if (size_ < 2)
		return ASCII;

	// Try to detect UTF-16
	if (src[0]==0xFF && src[1]==0xFE)
		return UTF16;
	else if (src[0]==0xFE && src[1]==0xFF)
		return UTF16BE;

	// We must have at least three characters for UTF-8
	if (size_ < 3)
		return ASCII;

	// Try to detect UTF-8
	if (src[0]==0xEF && src[1]==0xBB && src[2]==0xBF)
		return UTF8;

	// If no other encoding detected it must be ASCII
	return ASCII;
}

/* Opens the text file and converts to UTF16. */
bool Text::open(wchar_t* filename)
{
	// Try to open file
	file_.open(filename, READ);

	// If the file does not exist return false
	if (!file_.isReady())
		return false;

	// Read the file contents
	size_ = file_.size();
	buffer_ = new char[size_ + 2];
	file_.read(size_, buffer_);

	// Make the file contents a null terminated string (Unicode)
	buffer_[size_] = (unsigned char) 0;
	buffer_[size_ + 1] = (unsigned char) 0;

	// Detect text file encoding
	Encoding encoding = detectEncoding((unsigned char*) buffer_);

	// Check encoding type
	if (encoding == UTF16 || encoding == UTF16BE) {
		// Check that size is a multiple of two
		if (size_ & 1) {
			(error << L"File " << filename << " is UTF-16 but has odd byte count.").warning();
			delete [] buffer_;
			return false;
		}

		// Set text string
		if (encoding == UTF16)
			text_ = (utf16*) (buffer_);
		else
			text_ = UTF16BEtoUTF16((unsigned char*) buffer_);
		
		// Set length
		length_ = size_ / 2;

		// We will use the buffer as like an offset
		buffer_ = (char*) 1;
	}
	else if (encoding == ASCII) {
		text_ = ASCIItoUTF16((unsigned char*) buffer_, size_);
		length_ = size_;
		buffer_ = 0;
	}
	else if (encoding == UTF8) {
		text_ = UTF8toUTF16((unsigned char*) (buffer_ + 3));
		// ASCIItoUTF16 deletes buffer but UTF8toUTF16 does not
		// Need to clean this up at some stage...
		delete [] buffer_;
		buffer_ = 0;
	}

	// Create string with file contents, including b.o.m. if UTF16
	create(text_, length_);

	// Set state to true and return success
	setState(true);
	return true;
}

/* Frees the memory and zeros the pointers. */
void Text::close()
{
	// Reset state
	setState(false);

	// Free lines if needed
	if (lines)
		delete [] lines;

	// Reset variables
	initialize();

	// Destroy string
	String::destroy();
}

/* We need to overide destroy. */
void Text::destroy()
{
	close();
}

/* Converts from UTF8 to UTF16 */
// Perhaps make utf8 a type analogous to utf16 and utf32 in future...
utf16* Text::UTF8toUTF16 (unsigned char *utf8)
{
	int index = 0, out_index = 0;
    utf16 *out;
    unsigned char c;

    /* first calculate the size of the target string */
    c = utf8[index++];
    while(c) {
        if((c & 0x80) == 0) {
            index += 0;
        } else if((c & 0xe0) == 0xe0) {
            index += 2;
        } else {
            index += 1;
        }
        length_ += 1;
        c = utf8[index++];
    }   

    out = new utf16[length_ + 1];
	if (out == NULL)
        return NULL;
    index = 0;

    c = utf8[index++];
    while(c)
    {
        if((c & 0x80) == 0) {
            out[out_index++] = c;
        }
				else if((c & 0xe0) == 0xe0) {
            out[out_index] = (c & 0x1F) << 12;
           c = utf8[index++];
            out[out_index] |= (c & 0x3F) << 6;
           c = utf8[index++];
            out[out_index++] |= (c & 0x3F);
        }
				else {
            out[out_index] = (c & 0x3F) << 6;
           c = utf8[index++];
            out[out_index++] |= (c & 0x3F);
        }
        c = utf8[index++];
    }
    out[out_index] = 0;
    return out;
}

/* Converts from ASCII to UTF16 */
utf16* Text::ASCIItoUTF16 (unsigned char* ascii, int size)
{
	// Declare memory for new string and zero index
	utf16* out = new utf16[size + 1];
	int index = 0;
  
	// If there was no conversion
	if (out == NULL)
		return NULL;
	while (ascii[index]) {
		out[index] = ascii[index];
		index++;
	}
	out[index] = 0;
	delete [] ascii;
	return out;
}

/* Converts from big-endian UTF16 to little-endian UTF16 */
utf16* Text::UTF16BEtoUTF16 (unsigned char* unicodebe)
{
	// Initialize some variables
	int index = 0;
	unsigned char temp;

	// Swap every two bytes
	while (unicodebe[index] != 0 || unicodebe[index+1] != 0) {
		temp = unicodebe[index];
		unicodebe[index] = unicodebe[index + 1];
		unicodebe[index + 1] = temp;
		index += 2;
	}

	// Return adjusted string
	return (utf16*) unicodebe;
}

/* Return pointer to text. */
utf16* Text::string(void)
{
	return array_ + (int)buffer_;
}

/* Get text element, without bounds check (for speed). */
wchar_t& Text::operator[](unsigned int i)
{
	return Bucket::operator[](i + (int)buffer_);
}

/* Determine size of text. */
unsigned int Text::size()
{
	return Bucket::size() - (int)buffer_;
}

/* Splits the file string into an array of lines */
void Text::splitLines() 
{
	// Count how many lines in the file
	numberlines = 0;
	utf16* text = string();
	for (unsigned int i = 0;;) {
		numberlines++;
		if (!text[i = nextLine(text, i)]) break;
	}
	lines = new utf16*[numberlines];
	// Split into lines now
	int beforenextline = 0, linelen = 0;
	for (unsigned int i = 0, j = 0; j < numberlines; j++) {
		beforenextline = i;
		i = nextLine(text, i);
		linelen = i - beforenextline - whatNewLine(text, i);
		lines[j] = new wchar_t[linelen + 1];
		wcsncpy(lines[j], &text[beforenextline], linelen);
		lines[j][linelen] = 0;
	}
	// Free file string, don't neccesarily have to do this now, but it frees up some memory
	//delete [] text;
	//text = 0;
}

/* Breaks the text file into lines if necessary and then returns how many lines there are */
unsigned int Text::countLines()
{
	// if the file string was cleared then return 0 lines
	if (text_ == 0 && lines == 0) {
		numberlines = 0;
		return numberlines;
	}
	// if we need to split the file string into lines then do so
	if (lines == 0)
		splitLines();
	// return the number of lines
	return numberlines;
}

int compare (const void * a, const void * b)
{
  return wcscmp(*((utf16**)a), *((utf16**)b));
}


/* Sort lines */
void Text::sortLines()
{
	// if the file string was cleared then return 0 lines
	if (text_ == 0 && lines == 0)
		return;

	// if we need to split the file string into lines then do so
	if (lines == 0)
		splitLines();

	// Sort lines
	qsort(lines, numberlines, sizeof(utf16*), compare);
}

/* Returns a pointer to the string */
utf16* Text::getLine(unsigned int linenumber) 
{
	// make sure we don't go out of bounds with the array
	if (linenumber < numberlines) 
		return lines[linenumber];
	else
		return 0;
}

/* Returns lines */
utf16** Text::getLines()
{
	return lines;
}

unsigned int Text::nextLine(utf16* text, unsigned int i)
{
	while(text[i]!=0x0d && text[i]!=0x0a && text[i]!=0)
		i++;
	if ((text[i]==0x0d && text[i+1]==0x0a) || (text[i+1]==0x0d && text[i]==0x0a))
		i++;
	if (text[i])
		i++;
	return i;
}

unsigned int Text::whatNewLine(utf16* f, int i)
{
	if (i > 1) {
		if (((f[i-1] == 0x0d) && (f[i-2] == 0x0a)) || ((f[i-2] == 0x0d) && (f[i-1] == 0x0a)))
			return 2;
	}
	if (i > 0) {
		if (f[i-1] == 0x0d || f[i-1] == 0x0a)
			return 1;
	}
	return 0;
}